#include "Scene.h"

#define foreach(it,l) for(typeof(l.begin()) it=l.begin();it!=l.end();it++)

Scene::Scene(){

	ambient_light = 0;
	background = Color(0,0,0);
	recursion_limit = 1;
	max_ray_distance = 1000;

/*
	Material SOLID_BLACK;
	SOLID_BLACK.transparency = 0;
	SOLID_BLACK.reflection = 0;
	SOLID_BLACK.specular = 0;
	SOLID_BLACK.refraction = 0;
	SOLID_BLACK.color = Color(0,0,0);

	Material SOLID_RED;
	SOLID_RED.transparency = 0;
	SOLID_RED.reflection = 0;
	SOLID_RED.specular = 0;
	SOLID_RED.refraction = 0;
	SOLID_RED.color = Color(255,0,0);

	Material SOLID_GREEN;
	SOLID_GREEN.transparency = 0;
	SOLID_GREEN.reflection = 0;
	SOLID_GREEN.specular = 0;
	SOLID_GREEN.refraction = 0;
	SOLID_GREEN.color = Color(0,255,0);

	Material SOLID_BLUE;
	SOLID_BLUE.transparency = 0;
	SOLID_BLUE.reflection = 0;
	SOLID_BLUE.specular = 0;
	SOLID_BLUE.refraction = 0;
	SOLID_BLUE.color = Color(0,0,255);

	// cero y versores del espacio
	spheres.push_back(Sphere(Vector(30,0,0),10,SOLID_RED));
	spheres.push_back(Sphere(Vector(0,30,0),10,SOLID_GREEN));
	spheres.push_back(Sphere(Vector(0,0,30),10,SOLID_BLUE));
	spheres.push_back(Sphere(Vector(0,0,0),10,SOLID_BLACK));
*/
}

void Scene::set_camera(const Camara& c){
	camara = c;
}

void Scene::move_camara(const int d, const double l){
	camara.move(d,l);
}

void Scene::set_screen(int w, int h, double sw, double sh){
	screen_w = w;
	screen_h = h;
	sample_w = sw;
	sample_h = sh;
	sample_dw = (camara.direction()^camara.upside()).normalize();
	sample_dh = camara.upside();

}

void Scene::add_primitive(const Primitive* p){
	primitives.push_back(p);
}

void Scene::add_light(PointLight l){
	lights.push_back(l);
}

void Scene::set_ambient_light(double al){
	assert(0<=al && al<=1);
	ambient_light = al;
}

void Scene::set_background_color(Color c){
	background = c;
}

void Scene::set_recursion_limit(unsigned int l){
	recursion_limit = l;
}

void Scene::set_max_ray_distance(double d){
	max_ray_distance = d;
}

Color Scene::pixel(int x, int y){

	Ray r;
	r.origin = camara.position();

	// Calcula el punto del espacio donde se ubica el pixel de la pantalla
	Vector sample_d = camara.position() + (camara.direction()*camara.focal_length()) + (sample_dw*sample_w*x) + (sample_dh*sample_h*y);

	// calcula el versor del rayo de luz inverso (de la camara a la pantalla)
	r.direction = (sample_d - camara.position()).normalize();

	return traceRay(r,0);

}

Color Scene::traceRay(Ray r, unsigned int recursion_depth, const Primitive* tabu){

	double min_d = INFINITY;
	const Primitive* primitive;
	bool found = false;
	Vector I;
	Vector N;

	/* busco el objeto mas cercano */

	foreach(it,primitives){

		if( (*it) != tabu ){

			hitResult res =  (*it)->intersects(r,max_ray_distance);
			double new_d = res.distance;

			if(res.hit && new_d < min_d){

				found = true;
				min_d = new_d;
				primitive = (*it);

				// Calculo la normal al objeto
				I = r.origin + r.direction*res.distance;
				N = (*it)->normal(I);

			}
		}
	}

	/* Si existe alguno */

	if(found){

		/* Calculo el color diffuse y specular */

		Color res = primitive->color()*ambient_light;

		Color diffuse_color = Color(0,0,0);
		Color specular_color = Color(0,0,0);

		foreach(it2,lights){

			LightIntensity li = getLighting(primitive,*it2,I,r.direction,N);

			diffuse_color = diffuse_color + primitive->color()*li.diffuse*primitive->diffuse_reflection();

			specular_color = specular_color + it2->specularColor*li.specular*primitive->specular_reflection();

		}

		res = res + diffuse_color;

		/* Calculo el color del reflejo */

		if( recursion_depth < recursion_limit && primitive->reflection() > 0 ){

			Ray ref;
			ref.origin = I;
			ref.direction = r.direction.reflect(N);
			ref.origin = ref.origin + ref.direction*0.001;

			Color reflection_color = traceRay(ref,recursion_depth+1);

			res = reflection_color*primitive->reflection() + res*(1-primitive->reflection());

		}

		/* Calculo el color proveniente de la transparencia */
		/* OJO que no tiene depth de recursion. puede explotar? */

		if( primitive->transparency() > 0 ){

			Ray refr = primitive->refraction(I,r.direction);

			Color transparent_color = traceRay(refr,recursion_depth,primitive);

			res = transparent_color*primitive->transparency() + res*(1-primitive->transparency());

		}

		/* agrego el specular highlight */

		res = res + specular_color;

		// OJO habria que controlar que la distancia de la interseccion
		// sea menor a max_ray_distance, sino puede haber resultados raros
		double fog = sqrtf( 1 - ( I - camara.position() ).norm() / max_ray_distance );

		res = res*fog + background*(1-fog);

		return res;

	}else{

		return background;

	}
}

LightIntensity Scene::getLighting(const Primitive* p, const PointLight light, const Vector point, const Vector ray_d, const Vector normal){

	LightIntensity res;

	Vector light_d = light.position - point;	// rayo a la luz
	float distance = light_d.norm();			// norma del rayo a la luz
	//distance = distance*distance;				// para atenuar el efecto de la distancia
	light_d = light_d.normalize();				// me quedo con la direccion

	Ray r;
	r.origin = point;
	r.direction = light_d;

	/* Me fijo si esta en sombra */
	bool sh = false;
	foreach(it3,primitives) if(*it3!=p) if((*it3)->intersects(r,max_ray_distance).hit) sh = true;

	/* Calculo el aporte diffuse, el coseno del angulo entre la luz y la normal */

	double diffuse_intensity = light_d*normal;
	if(diffuse_intensity<0) diffuse_intensity = 0;
	if(1<diffuse_intensity) diffuse_intensity = 1;

	res.diffuse = (!sh)*(diffuse_intensity*light.diffusePower/*/distance*/);

	/* Calculo el aporte specular */

	// calculo la direccion del reflejo de la luz
	Vector refl = light_d.inverse().reflect(normal);

	// la intensidad es el coseno del angulo entre el reflejo y el rayo
	// elevado al brillo del material
	double specular_intensity = refl*ray_d.inverse();
	if(specular_intensity<0) specular_intensity = 0;
	if(1<specular_intensity) specular_intensity = 1;
	specular_intensity = pow(specular_intensity,p->shininess());

	res.specular = (!sh)*(specular_intensity*light.specularPower/*/distance*/);

	return res;

}

